DesseraLab

Technology exploration and development

0%

的确,Elixir 是一个好语言,它的函数式不如 Haskell 那么激烈,也不像 Rust 一类通用语言那样内敛。Elixir 的函数式是折中的,既要实用、又要优雅,同时,借由 Erlang VM 虚拟机的支持,它能够高性能地运行动态类型的代码。

Phoenix 是基于 Elixir 的 Web 框架,是我近期的学习重心。这个框架的学习资料很多,但中文资料较少且比较陈旧,我写这个系列文章的目的是记录我的学习内容。

路由流程

当我们访问 Phoenix 监听的端口,请求会经过 Router 传递给 Controller

让我们查看以下 Router 的定义(我的工程名为preflight,故模块名中带有Preflight):

该文件位于lib/${your_project_name}_web/router.ex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
defmodule PreflightWeb.Router do
use PreflightWeb, :router

pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, html: {PreflightWeb.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
end

pipeline :api do
plug :accepts, ["json"]
end

scope "/", PreflightWeb do
pipe_through :browser

get "/", PageController, :home
end

# Other scopes may use custom stacks.
# scope "/api", PreflightWeb do
# pipe_through :api
# end

# Enable LiveDashboard and Swoosh mailbox preview in development
if Application.compile_env(:preflight, :dev_routes) do
import Phoenix.LiveDashboard.Router

scope "/dev" do
pipe_through :browser

live_dashboard "/dashboard", metrics: PreflightWeb.Telemetry
forward "/mailbox", Plug.Swoosh.MailboxPreview
end
end
end

忽略大部分内容(因为现在的我看不懂),单单看这个模块的几个部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
defmodule PreflightWeb.Router do

# ...

# 域声明
scope "/", PreflightWeb do
pipe_through :browser

# 处理根目录的 get 请求
get "/", PageController, :home
end

# ...

end

真正定义了路由操作的是get "/", PageController, :home,它将访问根目录的GET请求交予PageContorller.home处理,我们来看一下该函数的定义:

该模块位于lib/${your_project_name}_web/controllers/page_controller.ex

1
2
3
4
5
6
7
defmodule PreflightWeb.PageController do
use PreflightWeb, :controller

def home(conn, _params) do
render(conn, :home, layout: false)
end
end

这个函数调用了render函数渲染页面。

等等,问题来了,页面呢?让我们重新审视以下这个函数,谁最像是那个页面?没错,一定是那个:home

这个原子变量指向PageHTML中声明的模板:

该模块位于lib/${your_project_name}_web/controllers/page_html.ex

1
2
3
4
5
6
7
8
9
10
defmodule PreflightWeb.PageHTML do
@moduledoc """
This module contains pages rendered by PageController.

See the `page_html` directory for all templates available.
"""
use PreflightWeb, :html

embed_templates "page_html/*"
end

该模块将page_html下的所有heex模板导入到当前目录,我们能够在该目录下发现home.html.heex,这就是我们真正的渲染对象。

路由宏

上文的get是 Phoenix 提供的宏,类似的,还有postputpatchdeleteoptionsconnecttracehead等。

我们可以通过mix phx.routes检查当前所有的路由:

1
2
3
4
5
6
7
8
9
10
11
mix phs.routes
GET / PreflightWeb.PageController :home
GET /dev/dashboard/css-:md5 Phoenix.LiveDashboard.Assets :css
GET /dev/dashboard/js-:md5 Phoenix.LiveDashboard.Assets :js
GET /dev/dashboard Phoenix.LiveDashboard.PageLive :home
GET /dev/dashboard/:page Phoenix.LiveDashboard.PageLive :page
GET /dev/dashboard/:node/:page Phoenix.LiveDashboard.PageLive :page
* /dev/mailbox Plug.Swoosh.MailboxPreview []
WS /live/websocket Phoenix.LiveView.Socket
GET /live/longpoll Phoenix.LiveView.Socket
POST /live/longpoll Phoenix.LiveView.Socket

除了默认的/路径外,还有一系列 Phoenix 预定义的路径。

resources

resources 是特殊的宏,目的是为了快速生成针对资源的增删改查:

1
2
3
4
5
6
7
8
9
10
defmodule PreflightWeb.Router do
# ...
scope "/", PreflightWeb do
pipe_through :browser
# ...

resources "/users", UserController
end
# ...
end

将生成以下路由:

1
2
3
4
5
6
7
8
GET     /users           PreflightWeb.UserController :index
GET /users/:id/edit PreflightWeb.UserController :edit
GET /users/new PreflightWeb.UserController :new
GET /users/:id PreflightWeb.UserController :show
POST /users PreflightWeb.UserController :create
PATCH /users/:id PreflightWeb.UserController :update
PUT /users/:id PreflightWeb.UserController :update
DELETE /users/:id PreflightWeb.UserController :delete

我们可以为该宏传入第三个配置参数来移除个别路由:

1
2
3
4
# 保留
resources "/posts", PostController, only: [:index, :show]
# 过滤
resources "/comments", CommentController, except: [:delete]

resources可以嵌套,将生成嵌套路由:

1
2
3
resources "/users", UserController do
resources "/posts", PostController
end

将生成以下结构:

1
2
3
4
5
6
7
8
GET     /users/:user_id/posts           PreflightWeb.PostController :index
GET /users/:user_id/posts/:id/edit PreflightWeb.PostController :edit
GET /users/:user_id/posts/new PreflightWeb.PostController :new
GET /users/:user_id/posts/:id PreflightWeb.PostController :show
POST /users/:user_id/posts PreflightWeb.PostController :create
PATCH /users/:user_id/posts/:id PreflightWeb.PostController :update
PUT /users/:user_id/posts/:id PreflightWeb.PostController :update
DELETE /users/:user_id/posts/:id PreflightWeb.PostController :delete

scope

scope可以声明 API 域:

1
2
3
4
scope "/admin, PreflightWeb do
pipe_through :browser
# ...
end

以上调用将该域内定义的请求路由至PreflightWeb对应的Controller中。

很长一段时间,我的工作环境都搭建在 Archlinux 上,不得不承认,Archlinux 是极其优秀的发行版,它的优秀体现在高度可定制性、完整的社区支持和丰富的软件源。这一切都很美好,直到我遇到了 NixOS。

NixOS 是一个特殊的 Linux 发行版,它基于一个名为 Nix 的 不可变包管理器,允许用户通过编写nix配置文件来构建可复现的操作系统。只需要写一份配置文件,然后在任何平台上都可以通过一些简单的命令重新构建出操作系统。听起来是一个很美好的愿景,不是吗?这正是 NixOS 的强大之处。

但凡事都有两面性,对于国内用户来说,NixOS 的学习门槛非常高,主要原因有以下几点:

  • 资料的分散和不完整
  • 资料的汉化程度不高

由于以上种种问题,通向 NixOS 之路困难重重。

本文的目的是记录我在使用 NixOS 时遇到的种种问题和它们的解决方法。

伊始 - 关于 NixOS 的安装

和其他的发行版一样,你需要下载 NixOS 的镜像文件,并制作启动媒介。前往该页面下载 NixOS 的镜像文件 NixOS下载,注意,我们要下载的是 NixOS 而不是 Nix。

在下载页面,我们有两种选择——图形化ISO和最简ISO,图形化ISO顾名思义,它可以用图形的方式进行系统安装,但缺点是可定制性很差;而最简ISO则可完全定义我们的安装流程。简单来说,如果你不想使用ext4或者想要深度定制一些内容,我更推荐使用最简ISO。

下载好镜像,我们将其制作为USB启动器(我使用的是Ventoy),从USB启动并进入镜像,我们就进入了 NixOS 的安装程序(或者说 LiveCD)。

详细的安装过程可以参考NixOS CN

Nix 配置文件

NixOS 采用名为 Nix 的函数式语言作为配置语言,在传统模式下,系统配置的根文件为/etc/nixos/configuration.nix,在 Flake 模式下,入口可以是任何位置。

下面是一个传统的configuration.nix配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 配置函数参数
{ config, lib, pkgs, ... }:

{
imports = [
# 导入子模块
# 硬件配置文件
./hardware-configuration.nix
];

# 各种配置项

system.stateVersion = "24.05";
}

配置 NixOS 的过程,就是修改这个文件,然后重新生成系统的过程。

例如,我们可以添加以下配置项以应用grub2

1
2
3
4
5
6
7
8
9
10
11
boot.loader = {
grub = {
enable = true;
device = "nodev";
efiSupport = true;
};
efi = {
canTouchEfiVariables = true;
efiSysMountPoint = "/boot";
};
};

Flake

传统的configuration.nix模式会从nix-channel配置的源中下载需要的包,这种模式实际上无法保证每次下载到的包是完全相同的,即无法保证可复现性。

为了解决这一问题,NixOS 引入了 Flake,简单来说,Flake 将软件源统一为了input变量,并将该变量传递给名为output的函数(将软件源的配置也变成了声明式)。同时,引入了现代构建系统的lock文件来锁定软件版本,保证每一次构建获得的软件版本相同。

Flake 配置的入口是flake.nix,下面是一个系统配置的入口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
# 工程的描述
description = "Dessera's NixOS configuration";

# 输入源配置
inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
};

# 输出函数定义
outputs = { self, nixpkgs, ... }@inputs: {
nixosConfigurations.nixos = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
# 将 configuration无缝迁移到flake配置
./configuration.nix
];
};
};
}

配置参考

要查看nix模块的所有配置选项,可以参考Nix Options

要查看pkgs中包含的包,可以参考Nix Packages

不变性实现

NixOS 依靠上述的配置文件不仅统一了系统级的大部分配置文件,而且为它的函数式模式提供了保障。我们实际上是编写了一个巨大的系统配置函数,并规定了它的输入,通过这种方式生成的系统因为输入和函数本身的不变性和无状态性,天然就保证了可复现性。

NixOS 中,我们可以通过以下命令生成新的系统,我们称之为世代

1
sudo nixos-rebuild switch

等待一段时间后,如果命令没有错误输出,我们便成功生成了新的世代,并切换到了新的系统。

没错,NixOS 的重新生成大部分情况下可以热重载。

如果不希望直接切换到新的系统,你可以将switch换为boot,它将在下次重启时切换到新的系统。

我们在配置文件中声明的任何内容,都将被下载到/nix/store中,包或者配置文件以${digest}-${name}的方式存储在该路径下,例如,下载的clang可能存在于:

1
/nix/store/afxm7yvnadvv9a3vcrhzjvnmfhdgbfc0-clang-18.1.4

该路径是一个只读文件系统,也就是所,我们不能在运行时更改该目录下的任何内容(除非通过nix),要清理该目录下没有使用的包,可以使用:

1
2
3
nix-collect-garbage -d
# 或者
nix store gc

在系统启动时,Nix 会根据当前选择的系统,将该目录下的内容动态链接至/run/current-system/中,并基于此启动系统。

例如,我们实际的sddm主题目录位于:

1
/run/current-system/sw/share/sddm/themes

说到现代C++,我们就不得不提到智能指针,这些模板类自C++11起被引入进标准库,不涉及操作系统、不涉及繁琐的内存申请流程,甚至你自己就能实现一个简单的智能指针。但即便如此,直到今天,仍然有人没有了解过,也不去使用智能指针。

实际上我认为,智能指针为 C++ 这门语言带来了一个新的概念,即所有权。智能指针表面上管理了一块由用户申请的内存,但实际上它是管理了这一块资源的所有权。这一概念在 Rust 语言中被更加明确地表达出来,但在 C++ 中,我们也可以通过智能指针来实现。

所有权

为什么会产生所有权机制?回想一下你在古老的 C/C++ 工作时,你不得不手动申请内存,手动管理他们的销毁时机,使用对象,还要时刻提防可能存在的拷贝操作(后来还要提防一些移动操作)。这些东西一开始就该由程序员管理,但是别忘了,我们在谈论一个现代语言!

在我看来,现代语言是高级语言的一个子集,它们应当在某一领域,解决过去程序员们的痛点。专注于底层的现代语言,如 Zig,它保留了内存管理的能力、并在语言层面支持了 Allocator, 扩充了内存管理的能力。而另一些现代语言,如 Rust,它在语言层面支持了所有权机制,使得程序员不再需要关心内存的申请和释放。

我们在谈论C++,从C++11开始,这门古老的语言开始了自己的现代化之路,而其中一个最显著的改变就是智能指针和其带来的所有权机制。

简单来说,所有权是一种属于关系,在这种关系下定义的对象其本身不会有隐式的复制和移动操作,将一个对象赋值、传递给另一个对象,只不过是将所有权转移给了另一个对象,在这个过程中,没有任何内存的拷贝和移动操作。

所有权的实现

对于 C++ 来说,所有权有两种,一种是独占所有权,另一种是共享所有权。独占所有权是指一个对象只能被一个所有者拥有,而共享所有权是指一个对象可以被多个所有者拥有。独占所有权在 C++ 中由 std::unique_ptr 实现,而共享所有权则由 std::shared_ptr 实现。

在一些场景(尤其是嵌入式领域)中,我们通常会针对每一个通信协议创建一个类,他们负责与其他设备的通信实现,而一些上层应用类,它们会需要调用这些协议类的方法。

创建这个协议类有两种方式:

  • 由应用类自己创建
  • 从外部创建,传递给应用类

但一般情况下,这些协议类的 IO 通道都是有限的,我们不希望应用类创建多个相同的协议类,这时候我们就可以使用智能指针来管理这些协议类的所有权。

由此可以引申出两种情况:

  • 一个协议类只服务于一个应用类,这时候我们可以使用 std::unique_ptr 来管理这个协议类的所有权
  • 一个协议类可能会被多个应用类使用,这时候我们可以使用 std::shared_ptr 来管理这个协议类的所有权

std::unique_ptr

std::unique_ptr 是一个独占所有权的智能指针,它只能有一个所有者,当这个所有者被销毁时,它所管理的资源也会被销毁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <memory>

class Protocol {
// ...
};

class Application {
public:
// 构造时,将协议类的所有权转移给 Application
Application(std::unique_ptr<Protocol> protocol) : protocol_(std::move(protocol)) {}

private:
std::unique_ptr<Protocol> protocol_;
};

int main() {
// 创建一个协议类,此时所有权掌握在 main 函数中
std::unique_ptr<Protocol> protocol = std::make_unique<Protocol>();

// 创建一个应用类,将协议类的所有权转移给 Application
Application app(std::move(protocol));
}

std::shared_ptr

std::shared_ptr 是一个共享所有权的智能指针,它可以有多个所有者,当最后一个所有者被销毁时,它所管理的资源也会被销毁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <memory>

class Protocol {
// ...
};

class Application {
public:
// 构造时,将协议类的所有权转移给 Application
Application(std::shared_ptr<Protocol> protocol) : protocol_(protocol) {}

private:
std::shared_ptr<Protocol> protocol_;
};

int main() {
// 创建一个协议类,此时所有权掌握在 main 函数中
std::shared_ptr<Protocol> protocol = std::make_shared<Protocol>();

// 创建一个应用类,将协议类的所有权转移给 Application
Application app(protocol);

// 创建另一个应用类,共享协议类的所有权
Application app2(protocol);
}

论所有权的转让与移动

很多人都有一个误解,认为移动语义体现了所有权的转让,但是,二者是不同的。

不论什么情况下,移动构造一定会重新创建资源对象,而至于资源内部的数据是如何移动的,这是对象的实现决定的,但针对资源对象本身,在这个过程中一定会重新创建一个资源对象。

而所有权的转让是另一回事,它直接将自己拥有的资源本身交予了另一个对象,在这个过程中,没有任何资源的拷贝和移动操作(实际上,在计算机眼中,这个过程什么也没发生)。

有趣的一点是,所有权的转让这一操作的实现依靠了移动语义,std::unique_ptr 不允许拷贝,但允许移动,刚才我们提到移动一定会创建新对象,而std::unqiue_ptr的移动虽然创建了新的std::unique_ptr,但其内部的指针仍然指向同一个资源对象,这就是所有权的转让的实现。

std::shared_ptr维护了一个引用计数,这让它可以进行拷贝,也可以进行移动,引用计数会时刻保证资源对象的生命周期,当最后一个所有者被销毁时,资源对象也会被销毁。

论资源的所有者和引用者

上文我们忽略的一种情况,有一些类或函数并不拥有资源,但他们需要暂时的资源使用权。对于我们已经建立的资源所有权模型,每一种所有权模式都有其对应的引用类型。

std::unique_ptr 对应的引用类型是原生指针,它指向资源对象本身,但在语义上不拥有它,只是暂时的使用。

std::shared_ptr 对应的引用类型是 std::weak_ptr,它指向资源对象,但不会增加引用计数,当资源对象被销毁时,std::weak_ptr 会被置空。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <memory>

class Protocol {
// ...
};

class Application {
public:
// 构造时,将协议类的所有权转移给 Application
Application(std::shared_ptr<Protocol> protocol) : protocol_(protocol) {}

private:
std::shared_ptr<Protocol> protocol_;
};

void foo(std::weak_ptr<Protocol> weak_protocol) {
// 使用 weak_ptr
}

int main() {
// 创建一个协议类,此时所有权掌握在 main 函数中
std::shared_ptr<Protocol> protocol = std::make_shared<Protocol>();

// 创建一个应用类,将协议类的所有权转移给 Application
Application app(protocol);

// 创建一个 weak_ptr,暂时使用协议类
std::weak_ptr<Protocol> weak_protocol = protocol;

// 调用函数,传递 weak_ptr
foo(weak_protocol);
}

值得注意的一点是,std::weak_ptr 不能直接使用,它需要通过 lock 方法转换为 std::shared_ptr 才能使用。

总结

新的资源管理模型标志着现代 C++ 的到来,我一直秉承着一个观点——任何拥有内存申请和释放的程序都应当使用智能指针,这不仅仅是为了避免内存泄漏,更是为了通过所有权这一概念更好地构建应用逻辑模型。

Nvidia 驱动程序,一个令 Linux 爱好者闻风丧胆的词语,它不同于开源的Nouveau驱动开箱即用,也不如AMD的显卡驱动那样安装简单。很多人——包括半年前的我,都禁不住疑惑,这个驱动为什么这么容易崩溃?

在我更换工作站到 Arch Linux 后,我终于有了机会去探索这个问题。实际上,如果你的大部分程序处于稳定版,并正确配置了所有关于显卡的设置,Nvidia 驱动程序完全可以正常工作。当然,我并不是说 Nvidia 驱动的崩溃全部都是使用者的问题,毕竟该驱动的不稳定性以及兼容性有目共睹。我只是想证明,Nvidia 驱动并不像传说中那么难以使用。

安装

不管在别的系统上如何折磨人,在 Arch Linux 的世界里,这一切都是如此简单,只要你使用的是一般的linux内核,安装驱动只需要一个命令:

1
sudo pacman -S nvidia nvidia-utils

如果你用的是其他内核、多内核甚至是自己编译的内核,你觉得会很复杂吗?不,只需要把上述命令中的nvidia换成nvidia-dkms即可。

当你重启之后,可能会有两个结果:一是你的系统正常启动,你看到了 SDDM 、GDM 或者 LightDM 的登录界面;二是你的系统卡在了启动界面,没有任何反应。

对于前者来说,恭喜你,你的驱动安装成功了。对于后者,也恭喜你,你的驱动安装成功了,只是你的系统没有正确加载驱动。这时候,你需要进入 tty,并查看一些配置是否正确。

这篇文章并不是一个错误排查指南,我只会告诉你一些浅显的东西,Nvidia 驱动如何加载、如何参与你桌面的启动、如何查看驱动是否加载等等。

Nvidia 驱动的加载流程

这里假定你使用的是 GRUB2 引导加载程序。

当你按下启动键,您首先看到的是您厂商的 BIOS/UEFI 启动界面,然后是 GRUB 启动界面。

什么是 GRUB?GRUB 是一个引导加载程序,它会让你选择你想要启动的系统(或者内核)。在这个阶段,Nvidia 驱动并没有加载。但你仍然可以添加一些参数来控制 Nvidia 驱动的行为,这些参数被称为内核引导参数。

最经典的莫过于nvidia_drm.modeset参数,它能够启用 DRM 内核级显示模式(这是一个显示管理系统,不在今天的讨论范围内)。你可以打开/etc/default/grub文件,找到GRUB_CMDLINE_LINUX_DEFAULT这一行,添加该条目:

1
GRUB_CMDLINE_LINUX_DEFAULT="loglevel=3 nowatchdog nvidia_drm.modeset=1"

然后运行sudo grub-mkconfig -o /boot/grub/grub.cfg来更新 GRUB 配置文件。

还有一种添加模块参数的方法,你可以在/etc/modprobe.d目录下创建一个文件,比如nvidia.conf,添加如下内容:

1
options nvidia-drm modeset=1

接下来是内核启动阶段,其存在一个早启动阶段,你可以指定这个阶段启动的模块,这些模块会最早加载到内核中。你可以打开/etc/mkinitcpio.conf文件,找到MODULES这一行,添加nvidia nvidia_modeset nvidia_uvm nvidia_drm这几个模块:

1
MODULES=(nvidia nvidia_modeset nvidia_uvm nvidia_drm)

然后运行sudo mkinitcpio -P来更新内核镜像。

如果这些内容没有被指定,一般情况下,显卡驱动可能会晚于显示管理器启动,这样会导致你的系统卡在启动界面。

与显示管理器的交互

这里必须要声明一点,显示管理器不是你的桌面!大部分情况下,它指代你的登录界面。

一般情况下,显示管理器根据其所使用的显示协议,有不同的方式来加载 Nvidia 驱动。

对于经典的Xorg服务器,它会先检测显卡驱动,并在大部分情况下选择 Nouveau 驱动。但且慢!我们没有提及一个重要的问题,nvidia软件包做了很多幕后工作,其中之一就是对 Xorg 服务器的配置。

值得一提的是,nvidia软件包会自动禁用 Nouveau 驱动,如果你手动移除了其针对 Xorg 的配置文件,Xorg毫无疑问地会加载失败。

你可以在/usr/share/X11/xorg.conf.d目录下找到一个名为10-nvidia-drm-outputclass.conf的文件,这个文件会告诉 Xorg 服务器使用 Nvidia 驱动。如果你没有这个文件,你可以手动创建一个:

1
2
3
4
5
6
7
8
Section "OutputClass"
Identifier "nvidia"
MatchDriver "nvidia-drm"
Driver "nvidia"
Option "AllowEmptyInitialConfiguration"
ModulePath "/usr/lib/nvidia/xorg"
ModulePath "/usr/lib/xorg/modules"
EndSection

这个文件会告诉 Xorg 服务器使用 Nvidia 驱动,而不是 Nouveau 驱动。

可笑的是,有相当一部分人会通过移除这个文件来”解决“Nvidia驱动的问题,这是一个错误的做法。

在这种情况下,通过 Xorg 启动桌面后,你会发现nvidia-smi命令的输出中永远都包含Xorg进程,这是完全正常的现象。

为什么?因为一切的页面都会通过 Xorg 服务器来显示,我们不可避免地会让软件运行在 Nvidia 显卡上(特别是对于游戏程序)。

而对于 Wayland 来说,一般来说设置内核引导参数就足够了,如果在这种情况下仍然无法进入桌面,请查看你的混成器文档。

多显卡

很多笔记本都是经典的双显卡设计,一个是 Intel 集成显卡,一个是 Nvidia 独立显卡。这种情况下,在上述配置完备的情况下,只要安装nvidia-prime软件包,你就可以指定程序使用哪个显卡。

1
sudo pacman -S nvidia-prime

要使用 Nvidia 显卡,你可以在程序启动时添加prime-run前缀:

1
prime-run glxgears

这样,你就可以在 Nvidia 显卡上运行程序了。

某些DM或者WM可能会默认使用Nvidia显卡作为渲染设备,为了功耗考虑,你可能需要手动设置以选择Intel显卡作为渲染设备。

对于 Hyprland 来说,它需要一个环境变量来指定使用哪个显卡:

1
export WLR_DRM_DEVICES=/dev/dri/card0

对于不同的DM或者WM,可能需要不同的设置,具体请查看相关文档。

总结

这篇文章并不是错误排查指南,也不是Nvidia驱动指南,正如我开篇提到的,本篇文章只是讲述一些浅显的内容,提供给你修复Nvidia显卡驱动问题的一些思路。

而对于更多的内容,你可以查看 Arch Wiki 上的 Nvidia 驱动页面,它包含了大量的内容,包括错误排查、多显卡设置、性能优化等等。

一直以来,我们管理 Python 项目的依赖都是通过手动创建虚拟环境,然后在虚拟环境中安装依赖。这种方式简单且有效,但如果读者尝试过 cargo、npm、yarn 等现代化的依赖管理工具,就会发现 Python 的依赖管理方式有些落后。

何出此言?我们来对比一下 cargo 和 conda 的依赖管理方式:

cargo针对每个项目都会创建一个 Cargo.toml 文件,用于记录项目的依赖信息。这样,我们只需要在 Cargo.toml 中添加依赖,然后执行 cargo build 即可安装依赖。这个过程是声明式的,我们随时可以看到项目的依赖信息,而这份信息也严格地与项目真正的依赖保持一致。

而对于 conda,即便有 environment.yml 文件与 requirements.txt 文件,我们也无法保证这份文件中的依赖与项目真正的依赖保持一致。往往我们改变了项目的依赖后,忘记更新这份文件,导致项目无法正常运行。

从以上的对比我们可以看出, conda 式的项目管理实际上是命令式的,这种以交互的方式构建项目环境的方式天生具有可变性,而 cargo 式的项目管理则是声明式的,这种方式实际上代表了不可变性。而后者是构建可复现系统的基础。

由此,我们引出了一个问题,如何让 Python 的依赖管理更像 cargo 呢?今天,我们介绍一个现代的 Python 依赖管理工具:Poetry。

Poetry

Poetry 是一个 Python 项目的依赖管理工具,它的目标是让 Python 项目的依赖管理更加现代化。Poetry 通过一个 pyproject.toml 文件来管理项目的依赖,这个文件类似于 cargo 的 Cargo.toml 文件。

要安装 Poetry,官方推荐使用 pipx

1
pipx install poetry

如果你正在使用 Arch Linux,你可以直接通过 pacman 安装 Poetry:

1
sudo pacman -S python-poetry

如果你想同时使用多个版本的 Poetry,你可以通过 asdf 来管理 Poetry 的版本:

1
2
3
asdf plugin add poetry
asdf install poetry latest # 安装最新版本的 Poetry
asdf global poetry latest # 设置全局的 Poetry 版本

安装完成后,我们可以通过 poetry --version 来查看 Poetry 的版本:

1
poetry --version

然后,我们使用 Poetry 来创建一个新的 Python 项目:

1
poetry new poetry-demo

这个命令会在当前目录下创建一个名为 poetry-demo 的 Python 项目。我们可以进入这个项目,然后查看整个项目结构:

1
2
3
4
5
6
7
.
├── poetry_demo
│   └── __init__.py
├── pyproject.toml
├── README.md
└── tests
└── __init__.py

pyproject.toml

我们先来熟悉一下 pyproject.toml 文件,以下是一个新建项目的 pyproject.toml 文件的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[tool.poetry]
name = "poetry-demo"
version = "0.1.0"
description = ""
authors = ["dessera <1533653159@qq.com>"]
readme = "README.md"

[tool.poetry.dependencies]
python = "^3.11"


[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

基本配置没有什么好说的,这里只强调一些重要的配置项,更多的配置项可以参考 官方文档

  • tool.poetry.package-mode:指定项目的性质,true 代表项目是一个库,false 代表项目是一个应用程序。
  • tool.poetry.dependencies:项目的依赖,这里我们可以指定项目的依赖,例如 requests = "^2.31.0"
  • build-system:构建系统的配置,可以指定构建系统。

还有一些进阶配置,比如依赖分组、依赖源管理、extra 等,但这些不适合在这篇文章中展开讨论。

添加依赖

我们可以通过 poetry add 命令来添加依赖,例如我们添加一个 requests 依赖:

1
poetry add requests

也可以直接在 pyproject.toml 文件中手动添加依赖:

1
2
3
[tool.poetry.dependencies]
python = "^3.11"
requests = "^2.31.0"

二者的区别是,poetry add 会自动同步虚拟环境中的依赖,而手动添加依赖则需要执行 poetry install 来同步虚拟环境中的依赖。

当添加好依赖后,我们可以执行 poetry run python 来进入虚拟环境,然后引入依赖来测试:

1
poetry run python
1
2
3
>>> import requests
>>> requests.__version__
'2.31.0'

构建项目

只有库项目才能使用 poetry build 命令来构建项目,这个命令会在项目根目录下生成一个 dist 目录,里面包含了项目的构建产物。

1
2
3
4
5
poetry build

# 查看构建产物
ls dist
poetry_demo-0.1.0-py3-none-any.whl poetry_demo-0.1.0.tar.gz

发布项目

如果我们想发布项目到 PyPI,我们可以使用 poetry publish 命令:

1
poetry publish

这个命令会将项目发布到 PyPI,当然,你需要先在 PyPI 上注册一个账号,鉴于现在的 PyPI 状态,这个步骤暂时无法完成。

作为应用程序

如果我们的项目是一个应用程序,我们可以使用 poetry run 命令来运行项目:

1
poetry run python -m poetry_demo

这需要我们在包中拥有一个 __main__.py 文件,这个文件会在我们执行 poetry run python -m poetry_demo 时被执行。

或者,你可以手动执行项目的入口文件:

1
poetry run python path/to/entry.py

该入口文件可以是任意 Python 文件。

环境兼容

Poetry 可以使用 requirements.txt 文件来兼容 pip,这样我们可以在 Poetry 与 pip 之间无缝切换。

1
poetry export -f requirements.txt > requirements.txt

这个命令会将项目的依赖导出到 requirements.txt 文件中,然后我们可以使用 pip 来安装这些依赖:

1
pip install -r requirements.txt

总结

Poetry 实际上更像是一个现代化的 Python 库管理工具,但个人认为,使用 Poetry 的意义在于让我们的项目更加现代化,更加可维护。Poetry 的出现,让我们的 Python 项目管理更加像 cargo,这是一个好的方向。

Poetry 还有诸多进阶功能,将在后续的文章中展开讨论。